/* eslint-disable */

/**
 * 数组虽然方便，但是链表也有一定都存在价值；
 * 比如：制作数据缓存，用户需要填写一份非常复杂的表单，用户发现错误，需要返回上一步。
 *
 * 代码参考自 Java 的 LinkedList，做了一定的调整，会有少许差异。
 *
 * 增加游标的设计：
 * 主要目的是为了缓存用户的操作记录，因此增加了一个游标（cursor），用于查看上一个、下一个元素。
 * 游标的移动，不会改变元素的值。
 * 相关函数：first/last/next/back
 *
 * @author Mr.css
 */
export default function LinkedList() {
    let size = 0;
    let first, last;

    /**
     * 一个用于在链表上移动的对象
     * 动态调整链表的过程中，移除元素的时候，需要注意游标是否在该对象上。
     */
    let cursor;

    /**
     * Tells if the argument is the index of an existing element.
     */
    function isElementIndex(index) {
        return index >= 0 && index < size;
    }

    /**
     * Tells if the argument is the index of a valid position for an
     * iterator or an add operation.
     */
    function isPositionIndex(index) {
        return index >= 0 && index <= size;
    }

    /**
     * Constructs an IndexOutOfBoundsException detail message.
     * Of the many possible refactorings of the error handling code,
     * this "outlining" performs best with both server and client VMs.
     */
    function outOfBoundsMsg(index) {
        return "Index: " + index + ", Size: " + size;
    }

    function checkElementIndex(index) {
        if (!isElementIndex(index))
            throw outOfBoundsMsg(index);
    }

    function checkPositionIndex(index) {
        if (!isPositionIndex(index))
            throw outOfBoundsMsg(index);
    }

    function getNode(index) {
        checkPositionIndex(index);
        if (index < (size >> 1)) {
            let x = first;
            for (let i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            let x = last;
            for (let i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    /**
     * Unlinks non-null first node f.
     */
    function unlinkFirst(f) {
        const element = f.item;
        const next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        return element;
    }

    /**
     * Unlinks non-null last node l.
     */
    function unlinkLast(l) {
        const element = l.item;
        const prev = l.prev;
        l.item = null;
        l.prev = null; // help GC
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        return element;
    }

    /**
     * Unlinks non-null last node l.
     */
    function unlink(x) {
        const element = x.item;
        const next = x.next;
        const prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        return element;
    }

    /**
     * Links e as first element.
     */
    function linkFirst(e) {
        const f = first;
        const newNode = {prev: null, item: e, next: f};
        first = newNode;
        if (f === undefined)
            last = newNode;
        else
            f.prev = newNode;
        size++;
    }

    /**
     * Links e as last element.
     */
    function linkLast(e) {
        const l = last;
        const newNode = {prev: l, item: e, next: null};
        last = newNode;
        if (l === undefined)
            first = newNode;
        else
            l.next = newNode;
        size++;
    }

    /**
     * Inserts element e before non-null Node succ.
     */
    function linkBefore(e, succ) {
        const pred = succ.prev;
        const newNode = {prev: pred, pred: e, next: succ};
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
    }

    return {
        /**
         * Returns the first element in this list.
         *
         * @return the first element in this list
         * @throws NoSuchElementException if this list is empty
         */
        getFirst: function () {
            const f = first;
            if (f == null)
                throw 'no such element';
            return f.item;
        },
        /**
         * Returns the last element in this list.
         *
         * @return the last element in this list
         */
        getLast: function () {
            const l = last;
            if (l == null)
                throw 'no such element';
            return l.item;
        },

        /**
         * Retrieves and removes the head (first element) of this list.
         *
         * @return the head of this list
         * @throws NoSuchElementException if this list is empty
         */
        removeFirst: function () {
            const f = first;
            if (f == null)
                throw 'no such element';
            return unlinkFirst(f);
        },

        /**
         * Removes and returns the last element from this list.
         *
         * @return the last element from this list
         * @throws NoSuchElementException if this list is empty
         */
        removeLast: function () {
            const l = last;
            if (l == null)
                throw 'no such element';
            return unlinkLast(l);
        },

        /**
         * Inserts the specified element at the beginning of this list.
         *
         * @param e the element to add
         */
        addFirst: function (e) {
            linkFirst(e);
        },
        /**
         * Appends the specified element to the end of this list.
         *
         * <p>This method is equivalent to {@link #add}.
         *
         * @param e the element to add
         */
        addLast: function (e) {
            linkLast(e);
        },

        /**
         * Returns true if this list contains the specified element.
         * More formally, returns true if and only if this list contains
         * at least one element {@code e} such that
         * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
         *
         * @param o element whose presence in this list is to be tested
         * @return if this list contains the specified element
         */
        contains: function (o) {
            return indexOf(o) !== -1;
        },

        /**
         * Returns the number of elements in this list.
         *
         * @return the number of elements in this list
         */
        size: function () {
            return size;
        },

        /**
         * Appends the specified element to the end of this list.
         *
         * <p>This method is equivalent to {@link #addLast}.
         *
         * @param e element to be appended to this list
         */
        add: function (e) {
            linkLast(e);
            return true;
        },

        /**
         * Appends all of the elements in the specified collection to the end of
         * this list, in the order that they are returned by the specified
         * collection's iterator.  The behavior of this operation is undefined if
         * the specified collection is modified while the operation is in
         * progress.  (Note that this will occur if the specified collection is
         * this list, and it's nonempty.)
         *
         * @param c collection containing elements to be added to this list
         * @return true if this list changed as a result of the call
         * @throws  if the specified collection is null
         */
        addAll: function (c) {
            return this.addAllIn(size, c);
        },

        /**
         * Inserts all of the elements in the specified collection into this
         * list, starting at the specified position.  Shifts the element
         * currently at that position (if any) and any subsequent elements to
         * the right (increases their indices).  The new elements will appear
         * in the list in the order that they are returned by the
         * specified collection's iterator.
         *
         * @param index index at which to insert the first element
         *              from the specified collection
         * @param a{[]} collection containing elements to be added to this list
         * @return true if this list changed as a result of the call
         * @throws IndexOutOfBoundsException {@inheritDoc}
         * @throws  if the specified collection is null
         */
        addAllIn: function (index, a) {
            checkPositionIndex(index);

            let numNew = a.length;
            if (numNew === 0)
                return false;

            let pred, succ;
            if (index === size) {
                succ = null;
                pred = last;
            } else {
                succ = getNode(index);
                pred = succ.prev;
            }

            for (let e in a) {
                let newNode = {prev: pred, item: e, next: null};
                if (pred == null)
                    first = newNode;
                else
                    pred.next = newNode;
                pred = newNode;
            }

            if (succ == null) {
                last = pred;
            } else {
                pred.next = succ;
                succ.prev = pred;
            }

            size += numNew;
            return true;
        },

        /**
         * Removes the first occurrence of the specified element from this list,
         * if it is present.  If this list does not contain the element, it is
         * unchanged.  More formally, removes the element with the lowest index
         * {@code i} such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
         * (if such an element exists).  Returns true if this list
         * contained the specified element (or equivalently, if this list
         * changed as a result of the call).
         *
         * @param o element to be removed from this list, if present
         */
        remove: function (o) {
            if (o == null) {
                for (let x = first; x != null; x = x.next) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (let x = first; x != null; x = x.next) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        },

        /**
         * Returns the element at the specified position in this list.
         *
         * @param index index of the element to return
         * @return the element at the specified position in this list
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        get: function (index) {
            checkElementIndex(index);
            return node(index).item;
        },

        /**
         * Replaces the element at the specified position in this list with the
         * specified element.
         *
         * @param index index of the element to replace
         * @param element element to be stored at the specified position
         * @return the element previously at the specified position
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        set: function (index, element) {
            checkElementIndex(index);
            let x = getNode(index);
            let oldVal = x.item;
            x.item = element;
            return oldVal;
        },

        /**
         * Inserts the specified element at the specified position in this list.
         * Shifts the element currently at that position (if any) and any
         * subsequent elements to the right (adds one to their indices).
         *
         * @param index index at which the specified element is to be inserted
         * @param element element to be inserted
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        addIn: function (index, element) {
            checkPositionIndex(index);

            if (index === size)
                linkLast(element);
            else
                linkBefore(element, getNode(index));
        },

        /**
         * Removes the element at the specified position in this list.  Shifts any
         * subsequent elements to the left (subtracts one from their indices).
         * Returns the element that was removed from the list.
         *
         * @param index the index of the element to be removed
         * @return the element previously at the specified position
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        removeIn: function (index) {
            checkElementIndex(index);
            return unlink(getNode(index));
        },

        // Queue operations.

        /**
         * Retrieves, but does not remove, the head (first element) of this list.
         *
         * @return the head of this list, or {@code null} if this list is empty
         */
        peek: function () {
            const f = first;
            return (f == null) ? null : f.item;
        },

        /**
         * Retrieves, but does not remove, the head (first element) of this list.
         *
         * @return the head of this list
         * @throws NoSuchElementException if this list is empty
         */
        element: function () {
            return getFirst();
        },

        /**
         * Retrieves and removes the head (first element) of this list.
         *
         * @return the head of this list, or {@code null} if this list is empty
         */
        poll: function () {
            const f = first;
            return (f == null) ? null : unlinkFirst(f);
        },

        /**
         * Adds the specified element as the tail (last element) of this list.
         *
         * @param e the element to add
         */
        offer: function (e) {
            return this.add(e);
        },

        // Deque operations
        /**
         * Inserts the specified element at the front of this list.
         *
         * @param e the element to insert
         */
        offerFirst: function (e) {
            this.addFirst(e);
            return true;
        },

        /**
         * Inserts the specified element at the end of this list.
         *
         * @param e the element to insert
         */
        offerLast: function (e) {
            this.addLast(e);
            return true;
        },

        /**
         * Retrieves, but does not remove, the first element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the first element of this list, or {@code null}
         *         if this list is empty
         * @since 1.6
         */
        peekFirst: function () {
            const f = first;
            return (f == null) ? null : f.item;
        },

        /**
         * Retrieves, but does not remove, the last element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the last element of this list, or {@code null}
         *         if this list is empty
         * @since 1.6
         */
        peekLast: function () {
            const l = last;
            return (l == null) ? null : l.item;
        },

        /**
         * Retrieves and removes the first element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the first element of this list, or {@code null} if
         *     this list is empty
         * @since 1.6
         */
        pollFirst: function () {
            const f = first;
            return (f == null) ? null : unlinkFirst(f);
        },

        /**
         * Retrieves and removes the last element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the last element of this list, or {@code null} if
         *     this list is empty
         * @since 1.6
         */
        pollLast: function () {
            const l = last;
            return (l == null) ? null : unlinkLast(l);
        },

        /**
         * Pushes an element onto the stack represented by this list.  In other
         * words, inserts the element at the front of this list.
         *
         * <p>This method is equivalent to {@link #addFirst}.
         *
         * @param e the element to push
         * @since 1.6
         */
        push: function (e) {
            this.addFirst(e);
        },

        /**
         * Pops an element from the stack represented by this list.  In other
         * words, removes and returns the first element of this list.
         *
         * <p>This method is equivalent to {@link #removeFirst()}.
         *
         * @return the element at the front of this list (which is the top
         *         of the stack represented by this list)
         * @throws NoSuchElementException if this list is empty
         * @since 1.6
         */
        pop: function () {
            return this.removeFirst();
        },

        /**
         * Removes the first occurrence of the specified element in this
         * list (when traversing the list from head to tail).  If the list
         * does not contain the element, it is unchanged.
         *
         * @param o element to be removed from this list, if present
         */
        removeFirstOccurrence: function (o) {
            return remove(o);
        },

        /**
         * Removes the last occurrence of the specified element in this
         * list (when traversing the list from head to tail).  If the list
         * does not contain the element, it is unchanged.
         *
         * @param o element to be removed from this list, if present
         */
        removeLastOccurrence: function (o) {
            if (o === null) {
                for (let x = last; x != null; x = x.prev) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (let x = last; x != null; x = x.prev) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        },
        /**
         * Returns the index of the first occurrence of the specified element
         * in this list, or -1 if this list does not contain the element.
         * More formally, returns the lowest index {@code i} such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
         * or -1 if there is no such index.
         *
         * @param o element to search for
         * @return the index of the first occurrence of the specified element in
         *         this list, or -1 if this list does not contain the element
         */
        indexOf: function (o) {
            let index = 0;
            if (o == null) {
                for (let x = first; x != null; x = x.next) {
                    if (x.item == null)
                        return index;
                    index++;
                }
            } else {
                for (let x = first; x != null; x = x.next) {
                    if (o === x.item)
                        return index;
                    index++;
                }
            }
            return -1;
        },

        /**
         * Returns the index of the last occurrence of the specified element
         * in this list, or -1 if this list does not contain the element.
         * More formally, returns the highest index {@code i} such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
         * or -1 if there is no such index.
         *
         * @param o element to search for
         * @return the index of the last occurrence of the specified element in
         *         this list, or -1 if this list does not contain the element
         */
        lastIndexOf: function (o) {
            let index = size;
            if (o == null) {
                for (let x = last; x != null; x = x.prev) {
                    index--;
                    if (x.item == null)
                        return index;
                }
            } else {
                for (let x = last; x != null; x = x.prev) {
                    index--;
                    if (o.equals(x.item))
                        return index;
                }
            }
            return -1;
        },

        /**
         * Removes all of the elements from this list.
         * The list will be empty after this call returns.
         */
        clear: function () {
            for (let x = first; x != null;) {
                let next = x.next;
                x.item = null;
                x.next = null;
                x.prev = null;
                x = next;
            }
            first = last = null;
            size = 0;
        },
        // 遍历链表
        forEach: function (call) {
            let idx = 0;
            let node = first;
            while (node.next !== null) {
                call(node, idx);
                node = node.next;
                idx++;
            }
        },
        // 映射成数组
        map: function (call) {
            let idx = 0;
            let node = first;
            let ret = [];
            while (node.next !== null) {
                let res = call(node, idx);
                if (res !== undefined) {
                    ret.push(res);
                }
                node = node.next;
                idx++;
            }
            return ret;
        },

        //游标时候有下一个元素
        hasNextItem: function () {
            return cursor.next !== null;
        },
        //游标是否有前一个元素
        hasPrevItem: function () {
            return cursor.prev !== null;
        },
        //游标当前指向的值
        currentValue: function () {
            return cursor.item;
        },
        //游标指向第一个元素
        firstItem: function () {
            cursor = first;
            return cursor.item;
        },
        //游标指向最后一个元素
        lastItem: function () {
            cursor = last;
            return cursor.item;
        },
        //游标指向后一个元素
        nextItem: function () {
            if (cursor.next !== null) {
                cursor = cursor.next;
                return cursor.item;
            }
        },
        //游标指向前一个元素
        prevItem: function () {
            if (cursor.pre !== null) {
                cursor = cursor.pre;
                return cursor.item;
            }
        }
    }
}

